package cn.geminis.data.jpa;

import jakarta.persistence.criteria.*;
import lombok.RequiredArgsConstructor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * @author puddi
 */
@RequiredArgsConstructor
public class QueryPredicateBuilder<T> {

    private final Map<String, Fetch<?, ?>> fetches = new HashMap<>();
    private final Map<String, Path<?>> paths = new HashMap<>();
    private final ArrayList<Predicate> predicates = new ArrayList<>();
    private final Root<T> root;
    private final CriteriaQuery<?> query;
    private final CriteriaBuilder criteriaBuilder;

    private Expression<?> getField(String propertyName) {
        return getPath(propertyName, true);
    }

    private Path<?> getPath(String propertyName, boolean isFirst) {
        if (paths.containsKey(propertyName)) {
            return paths.get(propertyName);
        } else {
            var index = propertyName.lastIndexOf(".");
            if (index > -1) {
                var parent = (Join<?, ?>) getPath(propertyName.substring(0, index), false);
                var currentName = propertyName.substring(index + 1);
                var path = isFirst ? parent.get(currentName) : parent.join(currentName, JoinType.LEFT);
                paths.put(propertyName, path);
                return path;
            } else {
                return isFirst ? root.get(propertyName) : root.join(propertyName, JoinType.LEFT);
            }
        }
    }

    private Fetch<?, ?> getFetch(String propertyName) {
        if (fetches.containsKey(propertyName)) {
            return fetches.get(propertyName);
        } else {
            var index = propertyName.lastIndexOf(".");
            Fetch<?, ?> fetch;
            if (index > -1) {
                var parent = getFetch(propertyName.substring(0, index));
                fetch = parent.fetch(propertyName.substring(index + 1), JoinType.LEFT);

            } else {
                fetch = root.fetch(propertyName, JoinType.LEFT);
            }
            fetches.put(propertyName, fetch);
            return fetch;
        }
    }

    public QueryPredicateBuilder<T> include(String[] include) {
        if (include != null) {
            for (var inc : include) {
                getFetch(inc);
            }
        }
        return this;
    }

    public QueryPredicateBuilder<T> equal(String propertyName, Object value) {
        if (value != null) {
            predicates.add(criteriaBuilder.equal(this.getField(propertyName), value));
        }
        return this;
    }

    public QueryPredicateBuilder<T> like(String propertyName, String value) {
        return like(propertyName, value, "%%%s%%");
    }

    public QueryPredicateBuilder<T> like(String propertyName, String value, String format) {
        if (value != null) {
            predicates.add(criteriaBuilder.like((Expression<String>) this.getField(propertyName), String.format(format, value)));
        }
        return this;
    }

    public QueryPredicateBuilder<T> greaterThan(String propertyName, Comparable value) {
        if (value != null) {
            predicates.add(criteriaBuilder.greaterThan((Expression<? extends Comparable>) this.getField(propertyName), value));
        }
        return this;
    }

    public QueryPredicateBuilder<T> greaterThanOrEqualTo(String propertyName, Comparable value) {
        if (value != null) {
            predicates.add(criteriaBuilder.greaterThanOrEqualTo((Expression<? extends Comparable>) this.getField(propertyName), value));
        }
        return this;
    }

    public QueryPredicateBuilder<T> lessThan(String propertyName, Comparable value) {
        if (value != null) {
            predicates.add(criteriaBuilder.lessThan((Expression<? extends Comparable>) this.getField(propertyName), value));
        }
        return this;
    }

    public QueryPredicateBuilder<T> lessThanOrEqualTo(String propertyName, Comparable value) {
        if (value != null) {
            predicates.add(criteriaBuilder.lessThanOrEqualTo((Expression<? extends Comparable>) this.getField(propertyName), value));
        }
        return this;
    }

    public QueryPredicateBuilder<T> in(String propertyName, Object[] value) {
        if (value != null) {
            predicates.add(this.getField(propertyName).in(value));
        }
        return this;
    }

    public QueryPredicateBuilder<T> isNull(String propertyName, Boolean value) {
        if (value != null && value) {
            predicates.add(criteriaBuilder.isNull(this.getField(propertyName)));
        }
        return this;
    }

    public QueryPredicateBuilder<T> isNotNull(String propertyName, Boolean value) {
        if (value != null && value) {
            predicates.add(criteriaBuilder.isNotNull(this.getField(propertyName)));
        }
        return this;
    }

    public QueryPredicateBuilder<T> isEmpty(String propertyName, Boolean value) {
        if (value != null && value) {
            predicates.add(criteriaBuilder.isEmpty((Expression<? extends Collection<?>>) this.getField(propertyName)));
        }
        return this;
    }

    public QueryPredicateBuilder<T> isNotEmpty(String propertyName, Boolean value) {
        if (value != null && value) {
            predicates.add(criteriaBuilder.isNotEmpty((Expression<? extends Collection<?>>) this.getField(propertyName)));
        }
        return this;
    }

    public Predicate build() {
        return criteriaBuilder.and(predicates.toArray(Predicate[]::new));
    }
}
